/*
 * Copyright 2016 Yuyou Chow
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.translation;

import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.documentation.DockablePopupManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.ide.DataManager;
import com.intellij.ide.actions.BaseNavigateToSourceAction;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.ide.util.gotoByName.ChooseByNameBase;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.AnActionListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.presentation.java.SymbolPresentationUtil;
import com.intellij.translation.translator.Translator;
import com.intellij.ui.ScrollingUtil;
import com.intellij.ui.content.Content;
import com.intellij.ui.popup.AbstractPopup;
import com.intellij.ui.popup.PopupPositionManager;
import com.intellij.util.Alarm;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.accessibility.AccessibleContextUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.lang.ref.WeakReference;
import java.util.List;

/**
 * Created by zyuyou on 16/6/20.
 *
 * https://github.com/JetBrains/intellij-community/blob/master/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java
 * @author JetBrains s.r.o.
 */
public class TranslationManager extends DockablePopupManager<TranslationComponent> {
	public static final ExtensionPointName<Translator> TRANSLATOR_EP = ExtensionPointName.create("com.intellij.translation.translator");

	@NonNls public static final String TRANSLATION_LOCATION_AND_SIZE = "com.intellij.translation.popup";
	public static final DataKey<String> SELECTED_QUICK_TRANSLATION_TEXT = DataKey.create("QUICK_TRANSLATION.SELECTED_TEXT");

	private static final Logger LOG = Logger.getInstance("#" + TranslationManager.class.getName());
	private static final String SHOW_TRANSLATION_IN_TOOL_WINDOW = "ShowTranslationInToolWindow";
	private static final String TRANSLATION_AUTO_UPDATE_ENABLED = "TranslationAutoUpdateEnabled";

	private Editor myEditor;
	private final Alarm myUpdateTranslationAlarm;
	private WeakReference<JBPopup> myTranslationHintRef;
	private Component myPreviouslyFocused;

	private final ActionManager myActionManager;

	private boolean myCloseOnSneeze;
	private ActionCallback myLastAction;
	private TranslationComponent myTestTranslationComponent;

	private AnAction myRestorePopupAction;

	public TranslationManager(final Project project, ActionManager manager) {
		super(project);

		myActionManager = manager;
		final AnActionListener actionListener = new AnActionListener() {
			@Override
			public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
				final JBPopup hint = getTranslationHint();
				if(hint != null){
					if(action instanceof HintManagerImpl.ActionToIgnore){
						((AbstractPopup)hint).focusPreferredComponent();
						return;
					}
					if(action instanceof ScrollingUtil.ListScrollAction) return;
					if(action == myActionManager.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN)) return;
					if(action == myActionManager.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_UP)) return;
					if(action == myActionManager.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_DOWN)) return;
					if(action == myActionManager.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_UP)) return;
					if(TranslationConstants.TRANSLATION_INPLACE_SETTINGS.equals(event.getPlace())) return;
					if(action instanceof BaseNavigateToSourceAction) return;
					closeTranslationHint();
				}
			}

			@Override
			public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
			}

			@Override
			public void beforeEditorTyping(char c, DataContext dataContext) {
				final JBPopup hint = getTranslationHint();
				// todo when typing
				if(hint != null){
					hint.cancel();
				}
			}
		};

		myActionManager.addAnActionListener(actionListener, project);
		myUpdateTranslationAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, myProject);
	}

	public static TranslationManager getInstance(Project project){
		return ServiceManager.getService(project, TranslationManager.class);
	}

	@Override
	protected String getShowInToolWindowProperty() {
		return SHOW_TRANSLATION_IN_TOOL_WINDOW;
	}

	@Override
	protected String getAutoUpdateEnabledProperty() {
		return TRANSLATION_AUTO_UPDATE_ENABLED;
	}

	@Override
	protected String getAutoUpdateTitle() {
		return "Auto-update";
	}

	@Override
	protected String getRestorePopupDescription() {
		return "Restore popup view mode";
	}

	@Override
	protected String getAutoUpdateDescription() {
		return "Refresh com.intellij.com.intellij.translation on selection change automatically";
	}

	@NotNull
	@Override
	protected AnAction createRestorePopupAction() {
		myRestorePopupAction = super.createRestorePopupAction();
		return myRestorePopupAction;
	}

	@Override
	protected void restorePopupBehavior() {
		if(myPreviouslyFocused != null){
			IdeFocusManager.getInstance(myProject).requestFocus(myPreviouslyFocused, true);
		}
		super.restorePopupBehavior();
		updateComponent();
	}

	@Override
	public void createToolWindow(PsiElement element, PsiElement originalElement) {
		super.createToolWindow(element, originalElement);

		// switch between toolWindow and popup
		if(myToolWindow != null){
			myToolWindow.getComponent().putClientProperty(ChooseByNameBase.TEMPORARILY_FOCUSABLE_COMPONENT_KEY, Boolean.TRUE);
			if(myRestorePopupAction != null){
				ShortcutSet quickTranslateShortCut = ActionManager.getInstance().getAction(TranslationConstants.ACTION_QUICK_TRANSLATE).getShortcutSet();
				myRestorePopupAction.registerCustomShortcutSet(quickTranslateShortCut, myToolWindow.getComponent());
				myRestorePopupAction = null;
			}
		}
	}



	@Override
	protected TranslationComponent createComponent() {
		return new TranslationComponent(this, createActions());
	}

	@Override
	protected void doUpdateComponent(PsiElement element, PsiElement originalElement, TranslationComponent component) {
		fetchTranslation(getDefaultCollector(myEditor.getSelectionModel().getSelectedText()), component);
	}

	@Override
	protected void doUpdateComponent(Editor editor, PsiFile psiFile) {
		showTranslation(editor, false, null);
	}

	@Override
	protected void doUpdateComponent(@NotNull PsiElement element) {
//		System.out.println("doUpdateComponent(@NotNull PsiElement element)");
	}

	@Override
	protected String getTitle(PsiElement element) {
		return getTitle(element, true);
	}

	static String getTitle(@Nullable final PsiElement element, final boolean _short) {
		return _short ? "": TranslationConstants.TOOL_WINDOW_ID;
	}

	@Override
	protected String getToolwindowId() {
		return TranslationConstants.TOOL_WINDOW_ID;
	}

	/**
	 * @return    <code>true</code> if quick doc control is configured to not prevent user-IDE interaction (e.g. should be closed if
	 *            the user presses a key);
	 *            <code>false</code> otherwise
	 */
	public boolean isMyCloseOnSneeze() {
		return myCloseOnSneeze;
	}

	@Nullable
	public JBPopup getTranslationHint() {
		if (myTranslationHintRef == null) return null;
		JBPopup hint = myTranslationHintRef.get();
		if (hint == null || !hint.isVisible() && !ApplicationManager.getApplication().isUnitTestMode()) {
			myTranslationHintRef = null;
			return null;
		}
		return hint;
	}

	private void closeTranslationHint() {
		JBPopup hint = getTranslationHint();
		if (hint == null) {
			return;
		}
		myCloseOnSneeze = false;
		hint.cancel();
		Component toFocus = myPreviouslyFocused;
		hint.cancel();
		if (toFocus != null) {
			IdeFocusManager.getInstance(myProject).requestFocus(toFocus, true);
		}
	}

	public void showTranslation(@NotNull Editor editor){
		showTranslation(editor, true, null);
	}

	public void showTranslation(@NotNull Editor editor, boolean requestFocus, @Nullable final Runnable closeCallback){
		myEditor = editor;

		SelectionModel selectionModel = editor.getSelectionModel();
		if(selectionModel.getSelectedText() != null){
			doShowTranslation(selectionModel.getSelectedText(), requestFocus, closeCallback);
		}
	}

	private void doShowTranslation(@NotNull String queryText, boolean requestFocus, @Nullable final Runnable closeCallback) {
		final Project project = myProject;
		if(!project.isOpen()) return;

		myPreviouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(project);

		JBPopup _oldHint = getTranslationHint();

		if(myToolWindow == null && PropertiesComponent.getInstance().isTrueValue(SHOW_TRANSLATION_IN_TOOL_WINDOW)){
			createToolWindow(null, null);
		}else if(myToolWindow != null){
			Content content = myToolWindow.getContentManager().getSelectedContent();
			if(content != null){
				TranslationComponent component = (TranslationComponent) content.getComponent();
				boolean samQuery = queryText.equals(component.getQuery());
				if(samQuery){
					JComponent preferredFocusableComponent = content.getPreferredFocusableComponent();
					// focus toolwindow on the second actionPerformed
					boolean focus = requestFocus || CommandProcessor.getInstance().getCurrentCommand() != null;
					if (preferredFocusableComponent != null && focus) {
						IdeFocusManager.getInstance(myProject).requestFocus(preferredFocusableComponent, true);
					}
				}else{
					content.setDisplayName(getTitle(null, true));
					fetchTranslation(getDefaultCollector(queryText), component, true);
				}
			}
			if(!myToolWindow.isVisible()){

					myToolWindow.show(null);
			}
		}else if(_oldHint != null && _oldHint.isVisible() && _oldHint instanceof AbstractPopup){
			TranslationComponent oldComponent = (TranslationComponent)((AbstractPopup) _oldHint).getComponent();
			fetchTranslation(getDefaultCollector(queryText), oldComponent);
		}else{
			showInPopup(queryText, requestFocus, closeCallback);
		}
	}

	private void showInPopup(@NotNull String queryText, boolean requestFocus, @Nullable final Runnable closeCallback){
		final TranslationComponent component = myTestTranslationComponent == null?
			new TranslationComponent(this) : myTestTranslationComponent;

		// todo NavigateCallback

		Processor<JBPopup> pinCallback = popup -> {
			createToolWindow(null, null);
			myToolWindow.setAutoHide(false);
			popup.cancel();
			return false;
		};

		ActionListener actionListener = e -> {
			createToolWindow(null, null);
			final JBPopup hint = getTranslationHint();
			if(hint != null && hint.isVisible()) hint.cancel();
		};

		List<Pair<ActionListener, KeyStroke>> actions = ContainerUtil.newSmartList();
		AnAction quickDocAction = ActionManager.getInstance().getAction(TranslationConstants.ACTION_QUICK_TRANSLATE);
		for (Shortcut shortcut : quickDocAction.getShortcutSet().getShortcuts()) {
			if (!(shortcut instanceof KeyboardShortcut)) continue;
			actions.add(Pair.create(actionListener, ((KeyboardShortcut)shortcut).getFirstKeyStroke()));
		}

		final JBPopup hint = JBPopupFactory.getInstance().createComponentPopupBuilder(component, component)
			.setProject(myProject)
			.setKeyboardActions(actions)
			.setDimensionServiceKey(myProject, TRANSLATION_LOCATION_AND_SIZE, false)
			.setResizable(true)
			.setMovable(true)
			.setRequestFocus(requestFocus)
			.setCancelOnClickOutside(true)
			.setTitle(getTitle(null, false))
			.setCouldPin(pinCallback)
			.setModalContext(false)
			.setCancelCallback(() -> {
				myCloseOnSneeze = false;
				if(closeCallback != null){
					closeCallback.run();
				}
				Disposer.dispose(component);
				myEditor = null;
				myPreviouslyFocused = null;
				return Boolean.TRUE;
			})
			.setKeyEventHandler(e -> {
				if(myCloseOnSneeze){
					closeTranslationHint();
				}
				if(AbstractPopup.isCloseRequest(e) && getTranslationHint() != null){
					closeTranslationHint();
					return true;
				}
				return false;
			})
			.createPopup();

		component.setHint(hint);

		fetchTranslation(getDefaultCollector(queryText), component);

		myTranslationHintRef = new WeakReference<>(hint);
	}

	public void fetchTranslation(final TranslationCollector provider, final TranslationComponent component){
		doFetchTranslation(component, provider, true, false);
	}

	public void fetchTranslation(final TranslationCollector provider, final TranslationComponent component, final boolean clearHistory){
		doFetchTranslation(component, provider, true, clearHistory);
	}

	private ActionCallback doFetchTranslation(final TranslationComponent component, final TranslationCollector provider, final boolean cancelRequests, final boolean clearHistory){
		final ActionCallback callback = new ActionCallback();
		myLastAction = callback;

		boolean wasEmpty = component.isEmpty();
		component.startWait();
		if(cancelRequests){
			myUpdateTranslationAlarm.cancelAllRequests();
		}
		if(wasEmpty){
			component.setText(TranslationBundle.message("translation.fetching.progress"), null, null);
			final AbstractPopup jbPopup = (AbstractPopup) getTranslationHint();
			if(jbPopup != null){
				jbPopup.setDimensionServiceKey(null);
			}
		}

		myUpdateTranslationAlarm.addRequest( () -> {
			if(myProject.isDisposed()) return ;
			LOG.debug("Started fetching com.intellij.translation...");
			final Throwable[] ex = new Throwable[1];
			String text = null;
			try{
				text = provider.getTranslation();
			}catch (Throwable e){
				LOG.info(e);
				ex[0] = e;
			}
			if(ex[0] != null){
				//noinspection SSBasedInspection
				SwingUtilities.invokeLater(() -> {
					String message = ex[0] instanceof IndexNotReadyException
						? "Translation is not available until indices are built."
						: TranslationBundle.message("translation.external.fetch.error.message");
					component.setText(message, null, null);
					callback.setDone();
				});
				return;
			}

			LOG.debug("Translation fetched successfully:\n", text);

			final String translationText = text;

			//noinspection SSBasedInspection
			SwingUtilities.invokeLater(() -> {
				PsiDocumentManager.getInstance(myProject).commitAllDocuments();

				if(translationText == null){
					component.setText(TranslationBundle.message("translation.no.info.found"), provider.getQuery(), null);
				}else if(translationText.isEmpty()){
					component.setText(component.getText(), provider.getQuery(), null);
				}else{
					component.setData(provider.getQuery(), translationText, provider.getTranslator());
				}

				final AbstractPopup jbPopup = (AbstractPopup) getTranslationHint();
				if(jbPopup == null){
					callback.setDone();
					return ;
				}

				jbPopup.setDimensionServiceKey(TRANSLATION_LOCATION_AND_SIZE);
				jbPopup.setCaption(getTitle(null, false));
				callback.setDone();
			});
		}, 10);

		return callback;
	}

	private TranslationCollector getDefaultCollector(final String queryText){
		return new DefaultTranslationCollector(queryText);
	}

	private interface TranslationCollector {
		@Nullable
		String getTranslation() throws Exception;
		@Nullable
		String getQuery();
		@Nullable
		String getExternalUrl();
		@Nullable
		Translator getTranslator();
	}

	private class DefaultTranslationCollector implements TranslationCollector {
		private final String myQuery;
		private String myExternalUrl;
		private Translator myTranslator;

		public DefaultTranslationCollector(String query) {
			this.myQuery = query;
		}

		@Nullable
		@Override
		public String getQuery() {
			return myQuery;
		}

		@Nullable
		@Override
		public String getExternalUrl() {
			return myExternalUrl;
		}

		@Nullable
		@Override
		public Translator getTranslator() {
			return myTranslator;
		}

		@Nullable
		@Override
		public String getTranslation() throws Exception {
			if(myQuery != null){
				for(Translator provider: TranslationManager.TRANSLATOR_EP.getExtensions()){
					final String translation = provider.fetchInfo(myQuery);
					if(translation != null){
						LOG.debug("Fetched translation from ", provider.getTitle());
						myExternalUrl = provider.getExternalUrl(myQuery);
						myTranslator = provider;
						return translation;
					}
				}
			}
			return null;
		}
	}

	void showHint(final JBPopup hint) {
		final Component focusOwner = IdeFocusManager.getInstance(myProject).getFocusOwner();
		DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner);
		PopupPositionManager.positionPopupInBestPosition(hint, myEditor, dataContext);
	}

	public Project getProject() {
		return myProject;
	}

}
